package com.ipan.kits.io;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import com.ipan.kits.text.EncodeUtil;

/**
 * ZIP的压缩和解压工具
 *
 * @author iPan
 * @date 2019-05-25
 */
public class ZipUtil {

    /**
     * zip压缩,如果参数校验失败则直接返回{@code false},默认采用UTF8编码
     *
     * @param zipPath 压缩后保存的文件路径
     * @param files   待压缩的文件列表
     */
    public static void pack(String zipPath, File... files) {
        pack(zipPath, StandardCharsets.UTF_8, files);
    }

    /**
     * zip压缩,如果参数校验失败则直接返回{@code false}
     *
     * @param zipPath  压缩后保存的文件路径
     * @param files    待压缩的文件列表
     * @param encoding zip文件编码
     */
    public static void pack(String zipPath, Charset encoding, File... files) {
        if (StringUtils.isNotBlank(zipPath)) pack(new File(zipPath), encoding, files);
    }

    /**
     * zip压缩,如果参数校验失败则直接返回{@code false},默认采用UTF8编码
     *
     * @param zipFile 压缩后保存的文件路径
     * @param files   待压缩的文件列表
     */
    public static void pack(File zipFile, File... files) {
        pack(zipFile, StandardCharsets.UTF_8, files);
    }

    /**
     * zip压缩,如果参数校验失败则直接返回{@code false}
     *
     * @param zipFile  压缩后保存的文件路径
     * @param files    待压缩的文件列表
     * @param encoding zip文件编码
     */
    public static void pack(final File zipFile, final Charset encoding, File... files) {
        Validate.notNull(zipFile, "ZipFile is null.");
        Validate.notNull(encoding, "Encoding is null.");
        Validate.notEmpty((File[])files, "Files is null or empty.");
        List<Path> pathList = new LinkedList<>();
        for (int i = 0, length = files.length; i < length; i++) {
            Validate.notNull(files[i], "The index[" + i + "] is null.");
            pathList.add(files[i].toPath());
        }
        Path[] paths = new Path[files.length];
        pathList.toArray(paths);
        pack(zipFile.toPath(), encoding, paths);
    }

    public static void pack(final Path zipPath, Path... paths) {
        pack(zipPath, StandardCharsets.UTF_8, paths);
    }

    public static void pack(final Path zipPath, final Charset encoding, Path... paths) {
        Validate.notNull(zipPath, "The zipPath is null.");
        Validate.notNull(encoding, "The encoding is null.");
        Validate.notNull(encoding, "The paths is null or empty.");
        validate(zipPath, paths);

        try (FileOutputStream fos = new FileOutputStream(zipPath.toFile());
             final ZipOutputStream zos = new ZipOutputStream(fos, encoding)) {

            for (Path path : paths) {
                final Path parent = path.getParent();
                Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

                    @Override
                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                        if (Files.size(dir) == 0) {
                            zos.putNextEntry(new ZipEntry(parent.relativize(dir).toString() + "/"));
                            zos.closeEntry();
                        }
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        zos.putNextEntry(new ZipEntry(parent.relativize(file).toString()));
                        Files.copy(file, zos);
                        zos.closeEntry();
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}，默认采用UTF8编码
     *
     * @param zipPath   zip文件路径
     * @param toDirPath 解压到指定目录
     */
    public static void unpack(String zipPath, String toDirPath) {
        unpack(zipPath, toDirPath, StandardCharsets.UTF_8);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}
     *
     * @param zipPath   zip文件路径
     * @param toDirPath 解压到指定目录
     * @param encoding  zip文件编码
     */
    public static void unpack(String zipPath, String toDirPath, Charset encoding) {
        if (StringUtils.isNotBlank(zipPath)) unpack(new File(zipPath), toDirPath, encoding);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}，默认采用UTF8编码
     *
     * @param zipPath   zip文件路径
     * @param toDirFile 解压到指定目录
     */
    public static void unpack(String zipPath, File toDirFile) {
        unpack(zipPath, toDirFile, StandardCharsets.UTF_8);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}
     *
     * @param zipPath   zip文件路径
     * @param toDirFile 解压到指定目录
     * @param encoding  zip文件编码
     */
    public static void unpack(String zipPath, File toDirFile, Charset encoding) {
        if (StringUtils.isNotBlank(zipPath)) unpack(new File(zipPath), toDirFile, encoding);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}，默认采用UTF8编码
     *
     * @param zipFile   zip文件路径
     * @param toDirFile 解压到指定目录
     */
    public static void unpack(File zipFile, File toDirFile) {
        unpack(zipFile, toDirFile, StandardCharsets.UTF_8);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}，默认采用UTF8编码
     *
     * @param zipFile   zip文件路径
     * @param toDirPath 解压到指定目录
     */
    public static void unpack(File zipFile, String toDirPath) {
        unpack(zipFile, toDirPath, StandardCharsets.UTF_8);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}
     *
     * @param zipFile   zip文件路径
     * @param toDirPath 解压到指定目录
     * @param encoding  zip文件编码
     */
    public static void unpack(File zipFile, String toDirPath, Charset encoding) {
        if (StringUtils.isNotBlank(toDirPath)) unpack(zipFile, new File(toDirPath), encoding);
    }

    /**
     * zip解压,如果参数校验失败则直接返回{@code false}
     *
     * @param zipFile   zip文件路径
     * @param toDirFile 解压到指定目录
     * @param encoding  zip文件编码
     */
    public static void unpack(final File zipFile, final File toDirFile, final Charset encoding) {
        Validate.notNull(zipFile, "The zipFile is null.");
        Validate.notNull(encoding, "The encoding is null.");
        Validate.notNull(toDirFile, "The toDirFile is null.");
        unpack(zipFile.toPath(), toDirFile.toPath(), encoding);
    }

    public static void unpack(final Path zipPath, final Path toDirPath) {
        unpack(zipPath, toDirPath, StandardCharsets.UTF_8);
    }

    public static void unpack(final Path zipPath, final Path toDirPath, final Charset encoding) {
        Validate.notNull(zipPath, "The zipPath is null.");
        Validate.notNull(encoding, "The encoding is null.");
        Validate.notNull(toDirPath, "The toDirPath is null.");
        if (!StringUtils.endsWithIgnoreCase(zipPath.getFileName().toString(), "zip"))
            throw new RuntimeException("The zipFile is not a zip file.");
        if (!zipPath.toFile().exists()) throw new RuntimeException("The zipFile does't exist.");
        if (toDirPath.toFile().exists() && !toDirPath.toFile().isDirectory())
            throw new RuntimeException("The toDirPath is not a directory.");

        FileSystemProvider provider = getZipFS();
        Map<String, Object> env = new HashMap<>();

        try (FileSystem fs = provider.newFileSystem(zipPath, env)) {
            extract(fs, toDirPath.toString(), null);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


//        try {
//            Files.walkFileTree(zipPath, new SimpleFileVisitor<Path>() {
//
//                @Override
//                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
//                    Files.createDirectories(toDirPath.resolve(dir));
//                    return FileVisitResult.CONTINUE;
//                }
//
//                @Override
//                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
//                    Path path = toDirPath.resolve(file);
//                    Files.copy(file, path);
//                    return FileVisitResult.CONTINUE;
//                }
//            });
//        } catch (IOException e) {
//            throw new RuntimeException(e);
//        }
    }

    public static void unpackx(final Path zipPath, String dir) {
        FileSystemProvider provider = getZipFS();
        Map<String, Object> env = new HashMap<>();

        try (FileSystem fs = provider.newFileSystem(zipPath, env)) {
            extract(fs, "/", Paths.get(dir));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private static void extract(FileSystem fs, String path, Path destPath) throws IOException {
        Path src = fs.getPath(path);
        if (Files.isDirectory(src)) {
            try (DirectoryStream<Path> ds = Files.newDirectoryStream(src)) {
                for (Path child : ds)
                    extract(fs, child.toString(), destPath);
            }
        } else {
            if (path.startsWith("/"))
                path = path.substring(1);
//            Path dst = FileSystems.getDefault().getPath(path);
            Path dst = destPath.resolve(path);
            Path parent = dst.getParent();
            if (Files.notExists(parent))
                mkdirs(parent);
            Files.copy(src, dst, REPLACE_EXISTING);
        }
    }

    private static void mkdirs(Path path) throws IOException {
        path = path.toAbsolutePath();
        Path parent = path.getParent();
        if (parent != null) {
            if (Files.notExists(parent))
                mkdirs(parent);
        }
        Files.createDirectory(path);
    }

    private static void validate(Path zipPath, Path... paths) {
        if (zipPath.toFile().isDirectory()) throw new RuntimeException("The zipPath is a directory.");
        Path zipParent = zipPath.getParent();
        for (Path path : paths) {
            if (!path.toFile().exists())
                throw new RuntimeException("The path[" + path.toString() + "] does not exist.");
            //当目标目录是原目录的子目录时,不支持压缩.
            if (path.toFile().isDirectory()) {
                if (zipParent.startsWith(path)) {
                    throw new RuntimeException(String.format("Destination [%s] is child directory of source [%s].", zipParent, path));
                }
            } else {
                if (zipParent.startsWith(path.getParent())) {
                    throw new RuntimeException(String.format("Destination [%s] is child directory of source [%s].", zipParent, path.getParent()));
                }
            }
        }
    }

    public static FileSystemProvider getZipFS() {
        for (FileSystemProvider fileSystemProvider : FileSystemProvider.installedProviders()) {
            if ("jar".equals(fileSystemProvider.getScheme())) {
                return fileSystemProvider;
            }
        }
        throw new RuntimeException("ZIP filesystem provider is not installed");
    }
    
    // -- 针对字符串的gzip解压缩 -- //
    /**
     * 使用gzip压缩字符串
     */
    public static byte[] gzipStr(String str) {
        if (str == null || str.length() < 1) {
            return null;
        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        GZIPOutputStream gzip = null;
        try {
            gzip = new GZIPOutputStream(out);
            gzip.write(str.getBytes());
        } catch (IOException e) {
            throw new RuntimeException("gzip exception", e);
        } finally {
            if (gzip != null) {
                try {
                    gzip.close();
                } catch (IOException e) {
                }
            }
        }
        return out.toByteArray();
    }
    /**
     * 使用gzip压缩字符串
     */
    public static String gzipStrToBase64(String str) {
    	byte[] buf = gzipStr(str);
    	if (buf == null || buf.length < 1) return "";
    	return EncodeUtil.encodeBase64(buf);
    }
    
    /**
     * 使用gzip解压字符串
     */
    public static String ungzipFromByte(byte[] compressed) {
    	if (compressed == null || compressed.length < 1) return "";
        String decompressed = null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = null;
        GZIPInputStream ginzip = null;
        try {
             in = new ByteArrayInputStream(compressed);
             ginzip = new GZIPInputStream(in);
             byte[] buffer = new byte[1024];
             int offset = -1;
             while ((offset = ginzip.read(buffer)) != -1) {
                 out.write(buffer, 0, offset);
             }
             decompressed = out.toString();
         } catch (IOException e) {
        	 throw new RuntimeException("ungzip exception", e);
         } finally {
             if (ginzip != null) {
                 try {
                     ginzip.close();
                 } catch (IOException e) {
                 }
             }
             if (in != null) {
                 try {
                     in.close();
                 } catch (IOException e) {
                 }
             }
             if (out != null) {
                 try {
                     out.close();
                 } catch (IOException e) {
                 }
             }
         }
         return decompressed;
    }
    /**
     * 使用gzip解压字符串
     */
    public static String ungzipFromBase64(String base64Str) {
        if (base64Str == null || base64Str.length() < 1) {
            return "";
        }
        byte[] compressed = null;
        String decompressed = null;
        compressed = new EncodeUtil().decodeBase64(base64Str);
        decompressed = ungzipFromByte(compressed);
        return decompressed;
    }

    // -- 针对字符串的zip解压缩 -- //
    /**
     * 使用zip压缩字符串
     */
    public static byte[] zipStr(String str) {
    	if (str == null || str.length() < 1) {
            return null;
        }
        byte[] compressed;
        ByteArrayOutputStream out = null;
        ZipOutputStream zout = null;
        try {
            out = new ByteArrayOutputStream();
            zout = new ZipOutputStream(out);
            zout.putNextEntry(new ZipEntry("0"));
            zout.write(str.getBytes());
            zout.closeEntry();
            compressed = out.toByteArray();
        } catch (IOException e) {
        	throw new RuntimeException("zip exception", e);
        } finally {
            if (zout != null) {
                try {
                    zout.close();
                } catch (IOException e) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
        return compressed;
    }
    /**
     * 使用zip压缩字符串
     */
    public static String zipStrToBase64(String str) {
    	byte[] buf = zipStr(str);
    	if (buf == null || buf.length < 1) return "";
    	return EncodeUtil.encodeBase64(buf);
    }
    
    /**
     * 使用zip解压字符串
     */
    public static String unzipFromByte(byte[] compressed) {
    	if (compressed == null || compressed.length < 1) return "";
        ByteArrayOutputStream out = null;
        ByteArrayInputStream in = null;
        ZipInputStream zin = null;
        String decompressed = null;
        try {
            out = new ByteArrayOutputStream();
            in = new ByteArrayInputStream(compressed);
            zin = new ZipInputStream(in);
            zin.getNextEntry();
            byte[] buffer = new byte[1024];
            int offset = -1;
            while ((offset = zin.read(buffer)) != -1) {
                out.write(buffer, 0, offset);
            }
            decompressed = out.toString();
        } catch (IOException e) {
        	throw new RuntimeException("unzip exception", e);
        } finally {
            if (zin != null) {
                try {
                    zin.close();
                } catch (IOException e) {
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
        return decompressed;
    }
    /**
     * 使用zip解压字符串
     */
    public static String unzipFromBase64(String base64Str) {
    	if (base64Str == null || base64Str.length() < 1) {
            return "";
        }
        byte[] compressed = null;
        String decompressed = null;
        compressed = new EncodeUtil().decodeBase64(base64Str);
        decompressed = unzipFromByte(compressed);
        return decompressed;
    }

    
    
    
    
    // -- 针对二进制的压缩/解压 -- //
    /**
     * 使用gzip压缩二进制
     */
    public static byte[] gzipByte(byte[] buf) {
        if (buf == null || buf.length < 1) {
            return null;
        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        GZIPOutputStream gzip = null;
        try {
            gzip = new GZIPOutputStream(out);
            gzip.write(buf);
        } catch (IOException e) {
            throw new RuntimeException("gzip exception", e);
        } finally {
            if (gzip != null) {
                try {
                    gzip.close();
                } catch (IOException e) {
                }
            }
        }
        return out.toByteArray();
    }
    /**
     * 使用gzip解压二进制
     */
    public static byte[] ungzipByte(byte[] compressed) {
    	if (compressed == null || compressed.length < 1) return null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = null;
        GZIPInputStream ginzip = null;
        try {
             in = new ByteArrayInputStream(compressed);
             ginzip = new GZIPInputStream(in);
             byte[] buffer = new byte[1024];
             int offset = -1;
             while ((offset = ginzip.read(buffer)) != -1) {
                 out.write(buffer, 0, offset);
             }
         } catch (IOException e) {
        	 throw new RuntimeException("ungzip exception", e);
         } finally {
             if (ginzip != null) {
                 try {
                     ginzip.close();
                 } catch (IOException e) {
                 }
             }
             if (in != null) {
                 try {
                     in.close();
                 } catch (IOException e) {
                 }
             }
             if (out != null) {
                 try {
                     out.close();
                 } catch (IOException e) {
                 }
             }
         }
         return out.toByteArray();
    }

    /**
     * 使用zip压缩二进制
     */
    public static byte[] zipByte(byte[] buf) {
    	if (buf == null || buf.length < 1) {
            return null;
        }
        byte[] compressed;
        ByteArrayOutputStream out = null;
        ZipOutputStream zout = null;
        try {
            out = new ByteArrayOutputStream();
            zout = new ZipOutputStream(out);
            zout.putNextEntry(new ZipEntry("0"));
            zout.write(buf);
            zout.closeEntry();
            compressed = out.toByteArray();
        } catch (IOException e) {
        	throw new RuntimeException("zip exception", e);
        } finally {
            if (zout != null) {
                try {
                    zout.close();
                } catch (IOException e) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
        return compressed;
    }
    
    /**
     * 使用zip解压字符串
     */
    public static byte[] unzipByte(byte[] compressed) {
    	if (compressed == null || compressed.length < 1) return null;
        ByteArrayOutputStream out = null;
        ByteArrayInputStream in = null;
        ZipInputStream zin = null;
        try {
            out = new ByteArrayOutputStream();
            in = new ByteArrayInputStream(compressed);
            zin = new ZipInputStream(in);
            zin.getNextEntry();
            byte[] buffer = new byte[1024];
            int offset = -1;
            while ((offset = zin.read(buffer)) != -1) {
                out.write(buffer, 0, offset);
            }
        } catch (IOException e) {
        	throw new RuntimeException("unzip exception", e);
        } finally {
            if (zin != null) {
                try {
                    zin.close();
                } catch (IOException e) {
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
        return out.toByteArray();
    }
    
}
